package sub2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Observable;
import java.util.Random;

import javax.swing.JTextField;

import exceptions.*;

/**
 * Keep the state of the client and handles the communication with the server
 * @author Magali Moulin, Pierre Labiausse
 * @version 2
 */

public class Client extends Observable {

	private String sharedFolderPath;
	private InetAddress serverAddress;
	private int serverPort, sharingPort;
	private LinkedList<String> sharedFiles = new LinkedList<String>();
	private HashMap<String, ArrayList<ClientTcpTuple>> searchResult = null;
	private ArrayList<String> searchRequest = null;
	
	/**
	 * Getter to have access to the list of searched keywords.
	 * @return searchRequest: a list of searched keywords
	 */
	public ArrayList<String> getSearchRequest() {
		return searchRequest;
	}
	
	/**
	 * Setter to put a new searched request with several keywords
	 * @param request: a list of searched keywords
	 */
	public void setSearchRequest(ArrayList<String> request) {
		this.searchRequest = request;
	}

	/**
	 * Getter to have access to the HashMap searchResult when there is a research.
	 * @return searchResult: the HashMap searchResult
	 */
	public HashMap<String, ArrayList<ClientTcpTuple>> getSearchResult() {
		return searchResult;
	}

	/**
	 * Getter to have access to the list of shared files
	 * @return sharedFiles: list of shared files
	 */
	public LinkedList<String> getSharedFiles() {
		return sharedFiles;
	}

	private LinkedList<String> subdirectories = new LinkedList<String>();
	private boolean registered;

	/**
	 * Set the parameters to the default values 
	 */
	public Client(){
		sharedFolderPath = Configuration.DEFAULT_SHARE_FOLDER_PATH;
		try {
			serverAddress = InetAddress.getByName("localhost");
			checkSharedFiles();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		serverPort = Configuration.DEFAULT_PORT;
		sharingPort = 8081 + (new Random()).nextInt(100);
		//sharingPort = 8088;
		System.out.println("Sharing port : "+sharingPort);
		registered = false;
		setChanged();
		notifyObservers();
		(new SharingServer(sharingPort, this)).start();
	}
	
	/**
	 *  Search the shared folder and its subdirectories and add the files encountered to the shared files 
	 */
	private LinkedList<String> listFiles(){
		LinkedList<String> result = new LinkedList<String>();
		subdirectories.add(sharedFolderPath);
		while(subdirectories.size() != 0){
			listFiles(subdirectories.poll(), result);
		}
		return result;
	}
	private void listFiles(String folderFilename, LinkedList<String> result){
		File shareFolder = new File(folderFilename);
		for(String filename : shareFolder.list()){
			File _tmp = new File(folderFilename+"/"+filename);
			if( ! _tmp.isDirectory()){
				result.add(filename);
			}
			else {
				subdirectories.add(folderFilename+"/"+filename);
			}
		}
	}

	/**
	 *  Change the path to the share folder
	 *  @param newPath : the new path of the shared folder
	 *  @throws SharedFolderNotFoundException
	 */
	public void changeSharedFolderPath(String newPath) throws SharedFolderNotFoundException{
		File file = new File(newPath);
		if(! (file.exists() && file.isDirectory()) ){
			throw new SharedFolderNotFoundException();
		}
		sharedFolderPath = newPath;
		checkSharedFiles();
		try {
			share(serverAddress, serverPort);
		} catch (FailureConnectionException e) {
			e.printStackTrace();
		}
		setChanged();
		notifyObservers();
	}
	
	/**
	 * Register the client with the server and send it the list of shared files
	 * Execute the Server part of the client, listening on the specified sharing port
	 * @param serverAddress: the address of the server used
	 * @param serverPort : the port of the server used
	 * @throws FailureConnectionException 
	 */
	public void share(InetAddress serverAddress, int serverPort) throws FailureConnectionException{
		// If we share already with another server, unshare with it
		if(isRegistered() && (!this.serverAddress.equals(serverAddress)||!(this.serverPort != serverPort) ) ){
			unshare();
		}
		this.serverAddress = serverAddress;
		this.serverPort = serverPort;
		Socket socket = null;
		ObjectOutputStream outObject = null;
		ObjectInputStream in = null;
		
		// Open connection with the server
		try {
			socket = new Socket(serverAddress, serverPort);
			socket.setSoTimeout(1000);
			outObject = new ObjectOutputStream(socket.getOutputStream());
			System.out.println("Client : outObject opened");
		} catch (IOException e) {
			try {
				if(outObject != null) outObject.close();
				if(socket != null) socket.close();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			throw new FailureConnectionException();
			
		}
		
		// Send the register message to the server
		try {
			outObject.writeObject(new String("register"));
			outObject.writeInt(sharingPort);
			outObject.writeObject(sharedFiles);	
			outObject.flush();

		} catch (IOException e1) {
			try {
				outObject.close();
				socket.close();
			} catch (IOException e2) {
				// TODO Auto-generated catch block
				e2.printStackTrace();
			}
			throw new FailureConnectionException();
		}
		
		// Listen for the response from the server
		try {
			in = new ObjectInputStream(socket.getInputStream());
			String _str = (String)in.readObject();
			System.out.println(_str);
			if(_str.equals("OK")){
				registered = true;
				setChanged();
				notifyObservers();
			}
			else {
				registered = false;
				setChanged();
				notifyObservers();
			}
			System.out.println("registered : "+registered);
			
		} catch (IOException e) {
			try {
				if(in != null) in.close();
				if(outObject != null) outObject.close();
				socket.close();
			} catch (IOException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			throw new FailureConnectionException();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		// Close the connection
		try {
			in.close();
			outObject.close();
			socket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Get the state of the client
	 * @return registered : registered or unregistered
	 */
	public boolean isRegistered() {
		return registered;
	}

	/**
	 * Unregister from the server
	 */
	public void unshare(){
		Socket socket = null;
		ObjectOutputStream out = null;
		ObjectInputStream in = null;
		
		// Open connection with the server
		try {
			socket = new Socket(serverAddress, serverPort);
			out = new ObjectOutputStream(socket.getOutputStream());
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		// Send the unregister message to the server
		try {
			
			out.writeObject(new String("unregister"));	
			out.writeInt(sharingPort);
			out.flush();
			System.out.println("send unregister to server");
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		
		// Listen for the response from the server
		try {
			in = new ObjectInputStream(socket.getInputStream());
			String _str;
			_str = (String) in.readObject();
			System.out.println(_str);
			if(_str.equals("OK")){
				registered = false;
				setChanged();
				notifyObservers();
			}
			else {
				registered = true;
				setChanged();
				notifyObservers();
			}
			System.out.println("registered : "+registered);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		// Close the connection
		try {
			in.close();
			out.close();
			socket.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Getter to have access to the address of the server.
	 * @return serverAddress: inetAddress of the server
	 */
	public InetAddress getServerAddress() {
		// TODO Auto-generated method stub
		return serverAddress;
	}

	/**
	 * Getter to have access to the address of the server
	 * @return serverPort: port of the server
	 */
	public int getServerPort() {
		// TODO Auto-generated method stub
		return serverPort;
	}

	/**
	 * Getter to have the path to the shared folder
	 * @return sharedFolderPath: the path of the shared folder
	 */
	public String getSharedFolderPath() {
		return sharedFolderPath;
	}

	/**
	 * Connect to the server to retrieve statistics about shared files and registered clients
	 * @param type: define the type of asked statistics (list of files or list of registered clients)
	 * @return result: list of the shared files on the server
	 */
	public ArrayList<String> serverStats(int type) {
		Socket socket = null;
		ObjectOutputStream out =  null;
		ObjectInputStream in = null;
		// Open connection with the server
		try {
			
			socket = new Socket(serverAddress, serverPort);	
			out = new ObjectOutputStream(socket.getOutputStream());			
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// If we want the list of shared files
		if(type == 0){
			// Send listFile message to the server
			try {
				System.out.println("Client -> list files");
				out.writeObject(new String("list files"));
				out.flush();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
		else{
			try {
				System.out.println("Client -> list registered clients");
				out.writeObject(new String("list registered clients"));	
				out.flush();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
		
		
		// Listen for the response from the server
		ArrayList<String> result = null;
		try {
			in = new ObjectInputStream(socket.getInputStream());
			result = (ArrayList<String>) in.readObject();
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (ClassNotFoundException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}	
		// Close the connection
		try {
			in.close();
			out.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return result;
	}
	
	/**
	 *  Try to retrieve a file by contacting a peer provided by the server
	 * @param solFile: destination File
	 * @param filename: name of the searched file
	 * @param peer: peer who must be contacted to retrieve the file
	 * @throws TestException
	 */
	public void retrieveFile(String filename, ClientTcpTuple peer, File solFile) throws TestException {
		Socket socket = null;
		PrintWriter out = null;
		ObjectInputStream in = null;
		
		// Connect to the peer and send him the filename
		try {
			socket = new Socket(peer.getInetAddress(), peer.getPort());
			out = new PrintWriter(socket.getOutputStream());
			out.println("request");
			out.flush();
			out.println(filename);
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}

		// Listen for the answer from the peer ("fileNotFound" || "fileFound")
		try {
			in = new ObjectInputStream(socket.getInputStream());
			String _response = null;
			while(_response == null){
				_response = (String)in.readObject();
			}
			if(_response.equals("fileNotFound")) {
				in.close();
				out.close();
				socket.close();
				throw new TestException();
			}
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
				e.printStackTrace();
		}

		// If the response was positive, receive the file and store it in the shared folder
		try {
			File file = (File)in.readObject();
			FileOutputStream outf = new FileOutputStream(solFile);
			FileInputStream inf = new FileInputStream(file);
			byte[] bytes = new byte[inf.available()];
			inf.read(bytes, 0, bytes.length);
			outf.write(bytes);
			outf.flush();
			outf.close();
			out.close();
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		// Check if we need to :
		// Update our entry in the server with our new file, reload the files shared displayed in the UI
		checkSharedFiles();
	}
	
	/**
	 *  Contact the server for a search 
	 *  @param request: list of the searched keywords
	 */
	public void searchRequest(ArrayList<String> txtSearch){
		Socket socket = null;
		ObjectOutputStream out;
		ObjectInputStream in;
		
		// Connect to the server and send the request command
		try {
			socket = new Socket(serverAddress, serverPort);
			out = new ObjectOutputStream(socket.getOutputStream());
			out.writeObject(new String("request"));
			out.flush();
			out.writeObject(txtSearch);
			searchRequest = txtSearch;
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		// Listen for an answer from the server (HashMap filename -> <peer1,peer2,...>
		try {
			in = new ObjectInputStream(socket.getInputStream());
			searchResult  = (HashMap<String, ArrayList<ClientTcpTuple>>) in.readObject();
			System.out.println("Number of results : "+searchResult.size());
			setChanged();
			notifyObservers();
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 *  Verify is the client needs to update its files to the server, and if yes and if the client is registered to a server, update the server
	 */
	public void checkSharedFiles(){
		LinkedList<String> sharedFilesNow = listFiles();
		if(!sharedFilesNow.equals(sharedFiles)){
			sharedFiles = sharedFilesNow;
			setChanged();
			notifyObservers();
			if(registered){
				try {
					share(serverAddress, serverPort);
				} catch (FailureConnectionException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

/**
 * Server part of the client, active while the client is registered with a server
 * 
 * @author Magali Moulin, Pierre Labiausse
 * @version 2
 *
 */
class SharingServer extends Thread {
	
	private Client client;
	private int sharingPort;
	
	/**
	 * Constructor of this class. 
	 * @param sharingPort: the own port of the client
	 * @param client: reference of the considered client.
	 */
	public SharingServer(int sharingPort, Client client){
		this.client = client;
		this.sharingPort = sharingPort;
	}

	@Override
	public void run() {
		ServerSocket sharingSocket = null;
		try {
			sharingSocket = new ServerSocket(sharingPort);
		} catch (IOException e) {
			e.printStackTrace();
		}
		while(true){
			try {
				Socket peerSocket = sharingSocket.accept();
				SharingHandler handler = new SharingHandler(peerSocket, client);
				handler.start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	
}